﻿// // This code is released under the BSD license.
namespace PugLib
{
	using System;
	using System.Collections.Generic;
	using System.Diagnostics.CodeAnalysis;
	using System.Reflection;
	using System.Threading;

	using PugLib.Threading;

	/// <summary>
	/// Repeat a method call.
	/// Taken from http://www.sharpregion.com/post/A-fluent-repeater.aspx
	/// </summary>
	[Serializable]
	public class Repeat : IRepeatOptions
	{
		#region Call

		/// <summary>
		/// A method with no parameters.
		/// </summary>
		public static IRepeatOptions Call(Action method)
		{
			return CreateRepeat(method);
		}

		/// <summary>
		/// A method with one parameter.
		/// </summary>
		public static IRepeatOptions Call<T1>(Action<T1> method)
		{
			return CreateRepeat(method);
		}

		/// <summary>
		/// A method with two parameters.
		/// </summary>
		public static IRepeatOptions Call<T1, T2>(Action<T1, T2> method)
		{
			return CreateRepeat(method);
		}

		/// <summary>
		/// A method with three parameters.
		/// </summary>
		public static IRepeatOptions Call<T1, T2, T3>(Action<T1, T2, T3> method)
		{
			return CreateRepeat(method);
		}

		/// <summary>
		/// A method with four parameters.
		/// </summary>
		public static IRepeatOptions Call<T1, T2, T3, T4>(Action<T1, T2, T3, T4> method)
		{
			return CreateRepeat(method);
		}

		/// <summary>
		/// A method with five parameters.
		/// </summary>
		public static IRepeatOptions Call<T1, T2, T3, T4, T5>(Action<T1, T2, T3, T4, T5> method)
		{
			return CreateRepeat(method);
		}

		/// <summary>
		/// A method with six parameters.
		/// </summary>
		public static IRepeatOptions Call<T1, T2, T3, T4, T5, T6>(Action<T1, T2, T3, T4, T5, T6> method)
		{
			return CreateRepeat(method);
		}

		/// <summary>
		/// A method with seven parameters.
		/// </summary>
		public static IRepeatOptions Call<T1, T2, T3, T4, T5, T6, T7>(Action<T1, T2, T3, T4, T5, T6, T7> method)
		{
			return CreateRepeat(method);
		}

		/// <summary>
		/// A method with eight parameters.
		/// </summary>
		public static IRepeatOptions Call<T1, T2, T3, T4, T5, T6, T7, T8>(Action<T1, T2, T3, T4, T5, T6, T7, T8> method)
		{
			return CreateRepeat(method);
		}

		/// <summary>
		/// A method with nine parameters.
		/// </summary>
		public static IRepeatOptions Call<T1, T2, T3, T4, T5, T6, T7, T8, T9>(Action<T1, T2, T3, T4, T5, T6, T7, T8, T9> method)
		{
			return CreateRepeat(method);
		}

		/// <summary>
		/// Calls method with argument generator the provdied number of times.
		/// </summary>
		/// <typeparam name="T1"></typeparam>
		/// <param name="method"></param>
		/// <param name="argument"></param>
		/// <param name="times"></param>
		public static void Call<T1>(Action<T1> method, Func<T1> argument, long times)
		{
			for (long i = 0; i < times; ++i)
			{
				method(argument());
			}
		}

		#endregion Call

		#region Fields

		private static readonly RepeatOptionsFactory RepeatOptionsFactory = new RepeatOptionsFactory();

		private readonly Delegate _method;
		private readonly List<Action<Exception>> _onExceptionCallList = new List<Action<Exception>>();
		private readonly List<Action> _onSuccessCallList = new List<Action>();
		private readonly ThreadHelper _threadHelper = new ThreadHelper();
		private bool _forever;
		private bool _inBackgroundThread;
		private bool _onExceptionThrow;
		private Action _onFinishCall;
		private object[] _parameters;

		private int? _pauseBetweenCallsMilliseconds;
		private bool _stopFlag;

		private ThreadPriority? _threadPriority;

		private int _times;
		private bool _untilFails;
		private bool _untilSucceeds;

		public static RepeatOptionsFactory RepeatFactory
		{
			get { return RepeatOptionsFactory; }
		}

		#endregion Fields

		#region Constructor

		/// <summary>
		/// Initializes a new instance of the <see cref="Repeat"/> class.
		/// </summary>
		/// <param name="method">The method to repeat.</param>
		internal Repeat(Delegate method)
		{
			if (method == null)
			{
				throw new ArgumentNullException("method");
			}

			_method = method;
		}

		#endregion Constructor

		#region IRepeatOptions Members

		/// <summary>
		/// set the parameters that will be passed to the method.
		/// </summary>
		/// <param name="parameters">The parameters.</param>
		/// <returns></returns>
		/// <exception cref="InvalidOperationException">If this method is called more than once for this instance.</exception>
		public IRepeatOptions WithParameters(params object[] parameters)
		{
			if (_parameters != null)
			{
				throw new InvalidOperationException("'WithParameters' has already been called for this instance.");
			}

			_parameters = parameters;

			return this;
		}

		/// <summary>
		/// Set the repeater to PauseBetweenCalls between each call.
		/// </summary>
		/// <param name="milliseconds">The milliseconds.</param>
		/// <returns></returns>
		/// <exception cref="InvalidOperationException">If this method is called more than once for this instance.</exception>
		/// <exception cref="ArgumentOutOfRangeException">If the milliseconds is less than zero.</exception>
		public IRepeatOptions PauseBetweenCalls(int milliseconds)
		{
			if (milliseconds < 0)
			{
				throw new ArgumentOutOfRangeException("milliseconds", "Value must be equal or greater than zero.");
			}

			if (_pauseBetweenCallsMilliseconds != null)
			{
				throw new InvalidOperationException("'PauseBetweenCalls' has already been called for this instance.");
			}

			_pauseBetweenCallsMilliseconds = milliseconds;

			return this;
		}

		/// <summary>
		/// Set the <see cref="ThreadPriority"/> of the thread that will invoke the method.
		/// </summary>
		/// <param name="priority">The thread priority.</param>
		/// <returns></returns>
		/// <exception cref="InvalidOperationException">If this method is called more than once for this instance.</exception>
		public IRepeatOptions At(ThreadPriority priority)
		{
			if (_threadPriority != null)
			{
				throw new InvalidOperationException("'At' has already been called for this instance.");
			}

			_threadPriority = priority;

			return this;
		}

		/// <summary>
		/// Set the repeater to call the method in a background thread.
		/// </summary>
		/// <value></value>
		/// <exception cref="InvalidOperationException">If this property is called more than once for this instance.</exception>
		public IRepeatOptions InBackgroundThread
		{
			get
			{
				if (_inBackgroundThread)
				{
					throw new InvalidOperationException("'InBackgroundThread' has already been called for this instance.");
				}

				_inBackgroundThread = true;

				return this;
			}
		}

		/// <summary>
		/// Set the repeater to stop when the call succeeds.
		/// </summary>
		/// <value></value>
		/// <exception cref="InvalidOperationException">If this property is called more than once for this instance.</exception>
		public IRepeatOptions UntilSucceeds
		{
			get
			{
				if (_untilSucceeds)
				{
					throw new InvalidOperationException("'UntilSucceeds' has already been called for this instance.");
				}

				_untilSucceeds = true;

				return this;
			}
		}

		/// <summary>
		/// Set the repeater to stop when the call fails.
		/// </summary>
		/// <value></value>
		/// <exception cref="InvalidOperationException">If this property is called more than once for this instance.</exception>
		public IRepeatOptions UntilFails
		{
			get
			{
				if (_untilFails)
				{
					throw new InvalidOperationException("'UntilFails' has already been called for this instance.");
				}

				_untilFails = true;

				return this;
			}
		}

		/// <summary>
		/// Sets the repeater to call a method, or a set of methods, upon success.
		/// </summary>
		/// <param name="method">The method.</param>
		/// <returns></returns>
		/// <exception cref="ArgumentNullException">if the method is null.</exception>
		public IRepeatOptions OnSuccessCall(Action method)
		{
			if (method == null)
			{
				throw new ArgumentNullException("method");
			}

			_onSuccessCallList.Add(method);

			return this;
		}

		/// <summary>
		/// Sets the repeater to call a method when it finishes the whole process successfuly.
		/// </summary>
		/// <param name="method">The method.</param>
		/// <returns></returns>
		/// <exception cref="ArgumentNullException">if the method is null.</exception>
		public IRepeatOptions OnFinishCall(Action method)
		{
			if (method == null)
			{
				throw new ArgumentNullException("method");
			}

			if (_onFinishCall != null)
			{
				throw new InvalidOperationException("'OnFinishCall' has already been called for this instance.");
			}

			_onFinishCall = method;

			return this;
		}

		/// <summary>
		/// Sets the repeater to call a method, or a set of methods, upon failure.
		/// This option can be set multiple times and all the methods will be called at the specified order.
		/// If no method is set with this method, then all exceptions will be thrown to the calling thread.
		/// </summary>
		/// <param name="method">The method.</param>
		/// <returns></returns>
		/// <exception cref="ArgumentNullException">if the method is null.</exception>
		public IRepeatOptions OnExceptionCall(Action<Exception> method)
		{
			if (method == null)
			{
				throw new ArgumentNullException("method");
			}

			_onExceptionCallList.Add(method);

			return this;
		}

		/// <summary>
		/// Set the repeater to stop when the call fails, and throw the exception.
		/// </summary>
		/// <value></value>
		/// <exception cref="InvalidOperationException">If this property is called more than once for this instance.</exception>
		public IRepeatOptions OnExceptionThrow
		{
			get
			{
				if (_onExceptionThrow)
				{
					throw new InvalidOperationException("'OnExceptionThrow' has already been called for this instance.");
				}

				_onExceptionThrow = true;

				return this;
			}
		}

		/// <summary>
		/// Starts the repeater and repeater the call forever.
		/// </summary>
		public void Start()
		{
			_forever = true;

			StartRepeat();
		}

		/// <summary>
		/// Starts the repeater and repeater the call the given amount of times.
		/// </summary>
		/// <param name="times">The times to repeater the call.</param>
		public void Start(int times)
		{
			if (times <= 0)
			{
				throw new ArgumentOutOfRangeException("times", "Value must be greater than zero.");
			}

			_times = times;

			StartRepeat();
		}

		#endregion

		#region Methods

		private static IRepeatOptions CreateRepeat(Delegate method)
		{
			IRepeatOptions repeat = RepeatOptionsFactory.CreateRepeat(method);
			return repeat;
		}

		private void StartRepeat()
		{
			if (_inBackgroundThread)
			{
				_threadHelper.QueueUserWorkItem(StartRepeat, null);
			}
			else
			{
				StartRepeat(null);
			}
		}

		private void StartRepeat(object state)
		{
			if (_inBackgroundThread)
			{
				if (_threadHelper.Name == null)
				{
					_threadHelper.Name = _method.Method.Name + "_Repeat";
				}
			}

			if (_threadPriority != null)
			{
				_threadHelper.Priority = _threadPriority.Value;
			}

			int time = 0;

			while (!_stopFlag)
			{
				Invoke();

				if (_forever)
				{
					continue;
				}

				++time;

				if (time >= _times)
				{
					_stopFlag = true;
				}
			}

			if (_onFinishCall != null)
			{
				_onFinishCall();
			}
		}

		private void Invoke()
		{
			try
			{
				_method.DynamicInvoke(_parameters);

				_onSuccessCallList.ForEach(a => a());

				if (_untilSucceeds)
				{
					_stopFlag = true;

					// Do not pause
					return;
				}
			}
			catch (TargetInvocationException ex)
			{
				HandleException(ex.InnerException);
			}
			catch (Exception ex)
			{
				HandleException(ex);
			}

			if (_pauseBetweenCallsMilliseconds != null)
			{
				_threadHelper.Sleep(_pauseBetweenCallsMilliseconds.Value);
			}
		}

		private void HandleException(Exception ex)
		{
			_onExceptionCallList.ForEach(a => a(ex));

			if (_onExceptionThrow)
			{
				throw ex;
			}

			if (!_untilFails)
			{
				return;
			}

			_stopFlag = true;
		}

		#endregion Methods
	}

	#region Types

	/// <summary>
	/// A factory class used for creating instances of the <see cref="Repeat"/> class.
	/// </summary>
	[Serializable]
	public class RepeatOptionsFactory
	{
		/// <summary>
		/// Initializes a new instance of the <see cref="Repeat"/> class.
		/// </summary>
		/// <param name="method">The method to repeat.</param>
		public virtual IRepeatOptions CreateRepeat(Delegate method)
		{
			return new Repeat(method);
		}
	}

	#endregion Types

	#region Delegates

	/// <summary>
	/// A method with five parameters
	/// </summary>
	[SuppressMessage("Microsoft.Design", "CA1005:AvoidExcessiveParametersOnGenericTypes")]
	public delegate void Action<T1, T2, T3, T4, T5>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5);

	/// <summary>
	/// A method with six parameters
	/// </summary>
	[SuppressMessage("Microsoft.Design", "CA1005:AvoidExcessiveParametersOnGenericTypes")]
	public delegate void Action<T1, T2, T3, T4, T5, T6>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6);

	/// <summary>
	/// A method with seven parameters
	/// </summary>
	[SuppressMessage("Microsoft.Design", "CA1005:AvoidExcessiveParametersOnGenericTypes")]
	public delegate void Action<T1, T2, T3, T4, T5, T6, T7>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7);

	/// <summary>
	/// A method with eight parameters
	/// </summary>    
	[SuppressMessage("Microsoft.Design", "CA1005:AvoidExcessiveParametersOnGenericTypes")]
	public delegate void Action<T1, T2, T3, T4, T5, T6, T7, T8>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8);

	/// <summary>
	/// A method with nine parameters
	/// </summary>    
	[SuppressMessage("Microsoft.Design", "CA1005:AvoidExcessiveParametersOnGenericTypes")]
	public delegate void Action<T1, T2, T3, T4, T5, T6, T7, T8, T9>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9);

	#endregion Delegates
}